use crate::args::PROXY;
use crate::db::{UserSub, SUBSCRIBE};
use crate::sref::Ref;
use crate::NFTP_INSTANCE_NAME;
use async_trait::async_trait;
use clap::ArgMatches;
use dashmap::DashMap;
use reqwest::{
    header::{HeaderMap, HeaderValue, CONTENT_TYPE}, Client,
    Proxy,
};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::path::Path;
use std::time::Duration;
use tracing::{error, info, warn};

#[async_trait]
pub trait EventDispatcher<T>: Send + Sync + Debug {
    async fn dispatch(&self, event: T);
}

#[async_trait]
impl EventDispatcher<FTPEvent> for HttpEventDispatcher {
    async fn dispatch(&self, event: FTPEvent) {
        match event.payload {
            FTPEventPayload::Put { path } => self.notify(event.username, path),
            _ => ()
        }
    }
}

#[derive(Debug)]
pub struct HttpEventDispatcher {
    dispatcher: Client,
    subscribe: &'static DashMap<String, UserSub>,
}

impl HttpEventDispatcher {
    pub fn new(arg_matches: &ArgMatches) -> Self {
        let builder = Client::builder();
        let builder = if let Some(proxy_addr) = arg_matches.get_one::<String>(PROXY) {
            match Proxy::all(proxy_addr) {
                Ok(proxy) => builder.proxy(proxy),
                Err(_) => {
                    warn!("Invalid proxy address {}", proxy_addr);
                    builder.no_proxy()
                }
            }
        } else {
            builder.no_proxy()
        };

        let mut headers = HeaderMap::new();
        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

        Self {
            dispatcher: builder
                .http1_only()
                .pool_idle_timeout(Some(Duration::from_secs(600)))
                .pool_max_idle_per_host(10)
                .connect_timeout(Duration::from_secs(5))
                .default_headers(headers)
                .build()
                .unwrap(),
            subscribe: SUBSCRIBE.get().unwrap(),
        }
    }

    pub fn notify(&self, username: Option<String>, path: String) {
        let Some(username) = username else {
            warn!(
                "{} belongs no user, maybe you are using anonymous login or misconfigured nftp",
                &path
            );
            return;
        };

        let Some(entry) = self.subscribe.get(&username) else {
            return;
        };
        let mut p = Path::new(&path);
        while let Some(parent) = p.parent() {
            let key = parent.to_str().unwrap();
            let user_sub = &entry.sub;
            let Some(urls) = user_sub.get(key) else {
                p = parent;
                continue
            };

            // notify data
            let data = NotifyEvent::new(path.as_str());
            let data = serde_json::to_string(&data).unwrap_or(Default::default());

            for url in urls.value() {
                let client = self.dispatcher.clone();
                let url = Ref::new(url);
                let path = path.clone();
                let username = username.clone();
                let data = data.clone();

                tokio::spawn(async move {
                    // .header(CONNECTION, "keep-alive")
                    match client.post(url.as_ref()).body(data).send().await {
                        Ok(res) => {
                            info!("Notify {} with {} for user {} result: {}", url.as_ref(), path, username, res.status());
                        }
                        Err(error) => {
                            error!(
                                "Error when notify {} with {} for user {}: {}",
                                url.as_ref(),
                                path,
                                username,
                                error
                            );
                        }
                    }
                });
            }
            break
        }
    }
}

#[derive(Serialize, Debug)]
struct NotifyEvent<'a> {
    source: &'static str,
    path: &'a str,
}

impl<'a> NotifyEvent<'a> {
    fn new(path: &'a str) -> Self {
        Self {
            source: NFTP_INSTANCE_NAME.get().unwrap(),
            path,
        }
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub struct FTPEvent {
    pub source_instance: String,
    pub hostname: String,
    pub payload: FTPEventPayload,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub username: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trace_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sequence_number: Option<u64>,
}

#[derive(Serialize, Deserialize, Debug)]
pub enum FTPEventPayload {
    Startup {
        libunftp_version: String,
        unftp_version: String,
    },
    Login {},
    Logout {},
    Get {
        path: String,
    },
    Put {
        path: String,
    },
    Delete {
        path: String,
    },
    MakeDir {
        path: String,
    },
    Rename {
        from: String,
        to: String,
    },
    RemoveDir {
        path: String,
    },
}
